#!/usr/bin/env node
import * as ts from 'typescript';
import glob from 'glob';
import fs from 'fs-extra';
import path from 'path';
import prettier from 'prettier';
import os from 'os';
import minimist from 'minimist';
import chalk from 'chalk';
import { execSync } from 'child_process';
import md5 from 'md5';
import dotenv from 'dotenv';
import {srcAliasPath} from './mod/tosrc'
import {version} from './package.json'

// 加载 .env 文件
dotenv.config();

process.on('unhandledRejection', (err) => {
  // @ts-ignore
  const stderr = err.stderr;

  if (stderr) {
    return console.log(stderr);
  }
  throw err;
});

const { _: cmdArgs, ...cmdOpt } = minimist(process.argv.slice(2));

const debug = (...msg: any[]) => {
  console.log(...msg);
  process.exit(1);
};

interface FileCacheInfo {
  /** 文件内容hash or 最后修改时间 */
  hash?: string;
  /** dts类型声明 */
  dts?: string;
}

type FileFullPath = string;


const isSingleDomain = Boolean(process.env?.CURRENT_DOMAIN);

export class DtsHelper {
  /** 缓存目录 */
  static cacheDir = path.resolve(os.homedir(), '.dtscache');
  /** 缓存实例 */
  cache: Record<FileFullPath, FileCacheInfo> = null;

  domain: string;

  constructor(domain: string) {
    this.domain = domain;
    this.initCache();
    this.getPackageJson();
  }

  /** 缓存绝对路径 */
  get cacheFileFullPath() {
    return path.resolve(DtsHelper.cacheDir, this.cacheFileName);
  }
  /** 缓存文件名 */
  get cacheFileName() {
    const dirname = path.basename(process.cwd());
    return md5([dirname, this.domain, 'cache'].join('_'));
  }
  /** 缓存目录绝对路径 */
  get cacheDirFullPath() {
    return path.dirname(this.cacheFileFullPath);
  }
  /** 类型声明文件名 */
  get dtsFileName() {
    return `${this.domain}.d.ts`;
  }
  /** 类型声明文件绝对路径 */
  get dtsFileFullPath() {
    return path.resolve(this.cacheDirFullPath, this.dtsFileName);
  }

  private _pkg: { name: string } = null;

  getPackageJson() {
    const pkgPath = path.join(process.cwd(), 'package.json');
    if (fs.existsSync(pkgPath)) {
      this._pkg = fs.readJsonSync(path.join(process.cwd(), 'package.json'));
    }
  }

  get packageName() {
    return this._pkg.name || 'unkown_package_name';
  }
  /** 初始化缓存 */
  private initCache() {
    fs.mkdirpSync(path.dirname(this.cacheFileFullPath));

    if (this.cache !== null) return this.cache;

    try {
      this.cache = JSON.parse(fs.readFileSync(this.cacheFileFullPath, { encoding: 'utf-8' }));
    } catch (err) {
      this.cache = {};
    }
    return this.cache;
  }

  /** 异步获取ts,tsx文件信息 */
  public async stat() {
    const globPattern = cmdOpt.all ? 'src/**/*.{ts,tsx}' : `src/${this.domain}/**/*.{ts,tsx}`;
    const files = glob.sync(globPattern, { absolute: true });

    const fileStats = await Promise.all(
      files
        .filter((f) => /\.d\.ts$/.test(f) === false)
        .map((file) => {
          return fs.stat(file).then((stat) => [file, stat.mtime.getTime().toString()]);
        })
    );
    return fileStats;
  }
  /** 文件差异列表， */
  async diff(useHash = false) {
    const diffFiles = useHash ? this.diffHash() : await this.diffMtime();
    diffFiles.forEach(([file, hash]) => {
      this.cache[file] = { hash };
    });
    return diffFiles;
  }
  /** 比较最后修改时间 */
  async diffMtime() {
    const fileStats = await this.stat();
    const cache = this.cache;
    const diffFiles = fileStats.filter(([file, hash]) => {
      if (!cache[file] || (cache[file] && cache[file].hash !== hash)) {
        return true;
      }
      return false;
    });
    return diffFiles;
  }
  /** 比较hash */
  diffHash() {
    const cache = this.cache;
    const domainDir = isSingleDomain ? 'src' : 'src/' + this.domain;

    console.log('domainDir: ' + domainDir);

    const shell = `git ls-files -s ${domainDir}| grep -nE ".tsx?$"`;
    const result = execSync(shell, { stdio: 'pipe' }).toString('utf-8');
    const diffFiles: string[][] = [];

    result.replace(/.+ +(.+?)\s+\d+\s+(.+)/gm, (_, hash, _file) => {
      const file = process.cwd() + '/' + _file;
      if (!cache[file] || cache[file].hash !== hash) {
        diffFiles.push([file, hash]);
      }
      return '';
    });
    return diffFiles;
  }

  updateCache(file: string, dts: string) {
    const cacheKey = this.cache[file] ? file : file + 'x';
    if (!this.cache[cacheKey]) {
      console.log(file + ' can not cached');
      return;
    }
    this.cache[cacheKey].dts = dts;
  }

  save() {
    fs.writeFileSync(this.cacheFileFullPath, JSON.stringify(this.cache));
    return this;
  }
  /**
   * 转换为 domain.d.ts
   * @param isPretty 是否格式化
   */
  toDTS(isPretty = true) {
    const output = Object.values(this.cache)
      .map((it) => it.dts)
      .join('\n');
    return isPretty
      ? prettier.format(output, { parser: 'typescript', printWidth: 120, useTabs: false, tabWidth: 4 })
      : output;
  }

  async createSourceFiles(files: string[]) {
    const sourceFiles = await Promise.all(
      files.map((file) => {
        return fs.readFile(file, { encoding: 'utf-8' }).then((content) => {
          return [file, ts.createSourceFile(file, content, ts.ScriptTarget.ESNext)];
        });
      })
    );
    return Object.fromEntries(sourceFiles);
  }

  /**
   * Create a compilerHost object to allow the compiler to read and write files
   * @param sourceFiles
   * @param writeFile
   */
  createCompilerHost(sourceFiles: Record<string, ts.SourceFile>) {
    const compilerHost: ts.CompilerHost = {
      getSourceFile: (filename: string) => sourceFiles[filename],
      writeFile: () => '',
      getDefaultLibFileName: () => 'lib.d.ts',
      useCaseSensitiveFileNames: () => false,
      getCanonicalFileName: (fileName) => fileName,
      getCurrentDirectory: () => '',
      getNewLine: () => '\n',
      fileExists: (fileName): boolean => !!sourceFiles[fileName],
      readFile: () => '',
      directoryExists: () => true,
      getDirectories: () => [],
    };
    return compilerHost;
  }

  async transpile(
    sourceFiles: Record<string, ts.SourceFile>,
    writeFile: ts.WriteFileCallback,
    options: ts.CompilerOptions
  ) {
    const compilerHost = this.createCompilerHost(sourceFiles);
    console.time('transpile:createProgram');
    const program = ts.createProgram(Object.keys(sourceFiles), options, compilerHost);
    console.timeEnd('transpile:createProgram');
    const emit = async (sourceFile: ts.SourceFile) => {
      // @ts-ignore
      // emit(targetSourceFile, writeFile, cancellationToken, emitOnlyDtsFiles, customTransformers, forceDtsEmit)
      return program.emit(sourceFile, writeFile, undefined, true, undefined, true);
    };
    console.time('transpile:emit');
    await Promise.all(
      Object.keys(sourceFiles).map((filename) => {
        return emit(sourceFiles[filename]);
      })
    );
    console.timeEnd('transpile:emit');
  }

  get repaceToRemote() {
    return isSingleDomain ? '@remote/' + this.domain : '@remote';
  }

  getModuleName(srcfullpath: string, fname: string) {
    let moduleName = fname.replace(srcfullpath, this.repaceToRemote);
    moduleName = moduleName.replace(/(\/index\.d\.ts|\.d\.ts)/, '');
    return moduleName;
  }
  toRemote(name: string, text: string) {
    // 将相对路径转换为src别名路径
    const srcAliasSource = srcAliasPath(name, text);
    // 替换src为 @remote
    return srcAliasSource.replace(/( declare |^declare )/gm, ' ').replace(/src\/(.+)/gm, this.repaceToRemote + '/$1');
  }

  async installDts() {
    const cwd = process.cwd();
    const domain: string = this.domain;
    try {
      fs.statSync(this.dtsFileFullPath);
    } catch (err) {
      return console.log(chalk.red(`\n 添加失败,请先到[${domain}]域所在工程执行 cdts build ${domain} \n`));
    }

    const dest = path.resolve(cwd, 'src/types', this.dtsFileName);
    await fs.mkdirp(path.dirname(dest));
    await fs.copyFile(this.dtsFileFullPath, dest);

    console.log(chalk.green(`\n${dest} 添加成功 \n`));
  }

  async writeFile(filename: string, output: string | fs.WriteFileOptions) {
    await fs.mkdirp(path.dirname(filename));
    return fs.writeFile(filename, output);
  }

  async buildDts() {
    console.time('total');
    const cwd = process.cwd();

    console.time('cache diff');
    const files = await this.diff(true);
    console.timeEnd('cache diff');

    console.log('diff count: ' + chalk.yellow(files.length));

    if (files.length === 0) return console.log(chalk.green('\n' + this.dtsFileFullPath + ' created successfull\n'));

    console.time('createSourceFiles');
    const sourceFiles = await this.createSourceFiles(files.map((it) => it[0]));
    console.timeEnd('createSourceFiles');

    const compilerOptions: ts.CompilerOptions = {
      sourceMap: false,
      lib: ['DOM', 'ScriptHost', 'ESNext'],
      skipLibCheck: true,
    };

    const srcfullpath = path.resolve(cwd, 'src');

    const writeFile = (name: string, text: string) => {
      const remoteModule = this.getModuleName(srcfullpath, name);
      const dtsCode = `declare module "${remoteModule}" {\n${this.toRemote(name, text)}}\n`;
      this.updateCache(name.replace(/\.d\.ts$/, '.ts'), dtsCode);
    };
    console.time('transpile:total');
    await this.transpile(sourceFiles, writeFile, compilerOptions);
    console.timeEnd('transpile:total');

    console.time('toDTS');
    const output = this.save().toDTS();
    console.timeEnd('toDTS');

    await this.writeFile(this.dtsFileFullPath, output);
    console.timeEnd('total');
    console.log('');
    console.log(chalk.green('\n' + this.dtsFileFullPath + ' created successfull\n'));
  }
}

function showHelp() {
  process.stdout.write(process.platform === 'win32' ? '\x1B[2J\x1B[0f' : '\x1B[2J\x1B[3J\x1B[H');
  console.log(`

  ${chalk.green('@chanjet/cjet-cmd')} ${chalk.yellow(version)} 
  
❓ 领域类型同步：

   cdts <add> domain
   
   ${chalk.yellow('cdts add apphub')}           install apphub.d.ts to src/types/apphub.d.ts
   ${chalk.yellow('cdts apphub')}               generate apphub.d.ts and save to ~/.dtscache/apphub.d.ts
   ${chalk.yellow('cdts components')}           generate components.d.ts and save to ~/.dtscache/components.d.ts
`);
}

async function main() {
  /* ---------------------------------- 参数解析 ---------------------------------- */

  let [subcmd = 'build', domain = process.env.CURRENT_DOMAIN] = cmdArgs;

  if (!['build', 'add'].includes(subcmd)) {
    domain = subcmd;
    subcmd = 'build';
  }

  const dtsHelper = new DtsHelper(domain);

  /* ---------------------------------- 帮助信息 ---------------------------------- */

  showHelp();

  /* --------------------------------- Install DTS -------------------------------- */

  if (subcmd === 'add') return dtsHelper.installDts();

  /* -------------------------------- Build DTS ------------------------------- */

  dtsHelper.buildDts();
}

main();
